summaryrefslogtreecommitdiff
path: root/tests/scripts/python
diff options
context:
space:
mode:
authorSébastien Helleu <flashcode@flashtux.org>2017-10-07 16:51:25 +0200
committerSébastien Helleu <flashcode@flashtux.org>2017-10-07 16:51:25 +0200
commite8af8536246bbda3289e70637f40dcba3c87977f (patch)
treee111538e2b5d8effd0db0bc7ca618743a73bbe02 /tests/scripts/python
parentf6fe6be7a4998ded6b6b4a7dbb9d1d12f3e8f65a (diff)
downloadweechat-e8af8536246bbda3289e70637f40dcba3c87977f.zip
tests: add scripting API tests (issue #104)
Automatic tests of scripting API are made with Python scripts: - unparse.py: convert Python code to other languages - testapigen.py: generate scripts in all languages to test the API - testapi.py scripting API tests
Diffstat (limited to 'tests/scripts/python')
-rw-r--r--tests/scripts/python/testapi.py131
-rwxr-xr-xtests/scripts/python/testapigen.py408
-rwxr-xr-xtests/scripts/python/unparse.py1259
3 files changed, 1798 insertions, 0 deletions
diff --git a/tests/scripts/python/testapi.py b/tests/scripts/python/testapi.py
new file mode 100644
index 000000000..7825d015b
--- /dev/null
+++ b/tests/scripts/python/testapi.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2017 Sébastien Helleu <flashcode@flashtux.org>
+#
+# This program 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 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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, see <http://www.gnu.org/licenses/>.
+#
+
+"""
+This script contains WeeChat scripting API tests
+(it can not be run directly and can not be loaded in WeeChat).
+
+It is parsed by testapigen.py, using Python AST (Abstract Syntax Trees),
+to generate scripts in all supported languages (Python, Perl, Ruby, ...).
+The resulting scripts can be loaded in WeeChat to test the scripting API.
+"""
+
+# pylint: disable=line-too-long,no-value-for-parameter
+
+import weechat # pylint: disable=import-error
+
+
+def check(result, condition, lineno):
+ """Display the result of a test."""
+ if result:
+ weechat.prnt('', ' TEST OK: ' + condition)
+ else:
+ weechat.prnt('',
+ 'SCRIPT_SOURCE' + ':' + lineno + ':1: ' +
+ 'ERROR: [' + 'SCRIPT_NAME' + '] condition is false: ' +
+ condition)
+
+
+def test_plugins():
+ """Test plugins functions."""
+ check(weechat.plugin_get_name('') == 'core')
+ check(weechat.plugin_get_name(weechat.buffer_get_pointer(weechat.buffer_search_main(), 'plugin')) == 'core')
+
+
+def test_strings():
+ """Test string functions."""
+ check(weechat.charset_set('iso-8859-15') == 1)
+ check(weechat.charset_set('') == 1)
+ check(weechat.iconv_to_internal('iso-8859-15', 'abc') == 'abc')
+ check(weechat.iconv_from_internal('iso-8859-15', 'abcd') == 'abcd')
+ check(weechat.gettext('abcdef') == 'abcdef')
+ check(weechat.ngettext('file', 'files', 1) == 'file')
+ check(weechat.ngettext('file', 'files', 2) == 'files')
+ check(weechat.strlen_screen('abcd') == 4)
+ check(weechat.string_match('abcdef', 'abc*', 0) == 1)
+ check(weechat.string_eval_path_home('test ${abc}', {}, {'abc': '123'}, {}) == 'test 123')
+ check(weechat.string_mask_to_regex('test*mask') == 'test.*mask')
+ check(weechat.string_has_highlight('my test string', 'test,word2') == 1)
+ check(weechat.string_has_highlight_regex('my test string', 'test|word2') == 1)
+ check(weechat.string_remove_color('test', '?') == 'test')
+ check(weechat.string_is_command_char('/test') == 1)
+ check(weechat.string_is_command_char('test') == 0)
+ check(weechat.string_input_for_buffer('test') == 'test')
+ check(weechat.string_input_for_buffer('/test') == '')
+ check(weechat.string_input_for_buffer('//test') == '/test')
+ check(weechat.string_eval_expression("100 > 50", {}, {}, {"type": "condition"}) == '1')
+ check(weechat.string_eval_expression("${buffer.full_name}", {}, {}, {}) == 'core.weechat')
+
+
+def test_lists():
+ """Test list functions."""
+ ptr_list = weechat.list_new()
+ check(ptr_list != '')
+ check(weechat.list_size(ptr_list) == 0)
+ item_def = weechat.list_add(ptr_list, 'def', weechat.WEECHAT_LIST_POS_SORT, '')
+ check(weechat.list_size(ptr_list) == 1)
+ item_abc = weechat.list_add(ptr_list, 'abc', weechat.WEECHAT_LIST_POS_SORT, '')
+ check(weechat.list_size(ptr_list) == 2)
+ check(weechat.list_search(ptr_list, 'abc') == item_abc)
+ check(weechat.list_search(ptr_list, 'def') == item_def)
+ check(weechat.list_search(ptr_list, 'ghi') == '')
+ check(weechat.list_search_pos(ptr_list, 'abc') == 0)
+ check(weechat.list_search_pos(ptr_list, 'def') == 1)
+ check(weechat.list_search_pos(ptr_list, 'ghi') == -1)
+ check(weechat.list_casesearch(ptr_list, 'abc') == item_abc)
+ check(weechat.list_casesearch(ptr_list, 'def') == item_def)
+ check(weechat.list_casesearch(ptr_list, 'ghi') == '')
+ check(weechat.list_casesearch(ptr_list, 'ABC') == item_abc)
+ check(weechat.list_casesearch(ptr_list, 'DEF') == item_def)
+ check(weechat.list_casesearch(ptr_list, 'GHI') == '')
+ check(weechat.list_casesearch_pos(ptr_list, 'abc') == 0)
+ check(weechat.list_casesearch_pos(ptr_list, 'def') == 1)
+ check(weechat.list_casesearch_pos(ptr_list, 'ghi') == -1)
+ check(weechat.list_casesearch_pos(ptr_list, 'ABC') == 0)
+ check(weechat.list_casesearch_pos(ptr_list, 'DEF') == 1)
+ check(weechat.list_casesearch_pos(ptr_list, 'GHI') == -1)
+ check(weechat.list_get(ptr_list, 0) == item_abc)
+ check(weechat.list_get(ptr_list, 1) == item_def)
+ check(weechat.list_get(ptr_list, 2) == '')
+ weechat.list_set(item_def, 'def2')
+ check(weechat.list_string(item_def) == 'def2')
+ check(weechat.list_next(item_abc) == item_def)
+ check(weechat.list_next(item_def) == '')
+ check(weechat.list_prev(item_abc) == '')
+ check(weechat.list_prev(item_def) == item_abc)
+ weechat.list_remove(ptr_list, item_abc)
+ check(weechat.list_size(ptr_list) == 1)
+ check(weechat.list_get(ptr_list, 0) == item_def)
+ check(weechat.list_get(ptr_list, 1) == '')
+ weechat.list_remove_all(ptr_list)
+ check(weechat.list_size(ptr_list) == 0)
+ weechat.list_free(ptr_list)
+
+
+def weechat_init():
+ """Main function."""
+ weechat.register('SCRIPT_NAME', 'SCRIPT_AUTHOR', 'SCRIPT_VERSION',
+ 'SCRIPT_LICENSE', 'SCRIPT_DESCRIPTION', '', '')
+ weechat.prnt('', '>>>')
+ weechat.prnt('', '>>> ------------------------------')
+ weechat.prnt('', '>>> Testing ' + 'SCRIPT_LANGUAGE' + ' API')
+ weechat.prnt('', ' > TESTS: ' + 'SCRIPT_TESTS')
+ test_plugins()
+ test_strings()
+ test_lists()
+ weechat.prnt('', ' > TESTS END')
diff --git a/tests/scripts/python/testapigen.py b/tests/scripts/python/testapigen.py
new file mode 100755
index 000000000..415898e99
--- /dev/null
+++ b/tests/scripts/python/testapigen.py
@@ -0,0 +1,408 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2017 Sébastien Helleu <flashcode@flashtux.org>
+#
+# This program 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 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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, see <http://www.gnu.org/licenses/>.
+#
+
+"""
+Scripts generator for WeeChat: build source of scripts in all languages to
+test the scripting API.
+
+This script can be run in WeeChat or as a standalone script
+(during automatic tests, it is loaded as a WeeChat script).
+
+It uses the following scripts:
+- unparse.py: convert Python code to other languages (including Python itself)
+- testapi.py: the WeeChat scripting API tests
+"""
+
+from __future__ import print_function
+import argparse
+import ast
+from datetime import datetime
+import inspect
+try:
+ from StringIO import StringIO # python 2
+except ImportError:
+ from io import StringIO # python 3
+import os
+import sys
+import traceback
+
+sys.dont_write_bytecode = True
+
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+sys.path.append(SCRIPT_DIR)
+from unparse import ( # pylint: disable=wrong-import-position
+ UnparsePython,
+ UnparsePerl,
+ UnparseRuby,
+ UnparseLua,
+ UnparseTcl,
+ UnparseGuile,
+ UnparseJavascript,
+ UnparsePhp,
+)
+
+RUNNING_IN_WEECHAT = True
+try:
+ import weechat
+except ImportError:
+ RUNNING_IN_WEECHAT = False
+
+
+SCRIPT_NAME = 'testapigen'
+SCRIPT_AUTHOR = 'Sébastien Helleu <flashcode@flashtux.org>'
+SCRIPT_VERSION = '0.1'
+SCRIPT_LICENSE = 'GPL3'
+SCRIPT_DESC = 'Generate scripting API test scripts'
+
+SCRIPT_COMMAND = 'testapigen'
+
+
+class WeechatScript(object): # pylint: disable=too-many-instance-attributes
+ """
+ A generic WeeChat script.
+
+ This class must NOT be instanciated directly, use subclasses instead:
+ PythonScript, PerlScript, ...
+ """
+
+ def __init__(self, unparse_class, tree, source_script, output_dir,
+ language, extension, comment_char='#',
+ weechat_module='weechat'):
+ # pylint: disable=too-many-arguments
+ self.unparse_class = unparse_class
+ self.tree = tree
+ self.source_script = os.path.realpath(source_script)
+ self.output_dir = os.path.realpath(output_dir)
+ self.language = language
+ self.extension = extension
+ self.script_name = 'testapi.%s' % extension
+ self.script_path = os.path.join(self.output_dir, self.script_name)
+ self.comment_char = comment_char
+ self.weechat_module = weechat_module
+ self.rename_functions()
+ self.replace_variables()
+
+ def comment(self, string):
+ """Get a commented line."""
+ return '%s %s' % (self.comment_char, string)
+
+ def rename_functions(self):
+ """Rename some API functions in the tree."""
+ functions = {
+ 'prnt': 'print',
+ 'prnt_date_tags': 'print_date_tags',
+ 'prnt_y': 'print_y',
+ }
+ for node in ast.walk(self.tree):
+ if isinstance(node, ast.Call) and \
+ isinstance(node.func, ast.Attribute) and \
+ node.func.value.id == 'weechat':
+ node.func.attr = functions.get(node.func.attr, node.func.attr)
+
+ def replace_variables(self):
+ """Replace script variables in string values."""
+ variables = {
+ 'SCRIPT_SOURCE': self.source_script,
+ 'SCRIPT_NAME': self.script_name,
+ 'SCRIPT_PATH': self.script_path,
+ 'SCRIPT_AUTHOR': 'Sebastien Helleu',
+ 'SCRIPT_VERSION': '1.0',
+ 'SCRIPT_LICENSE': 'GPL3',
+ 'SCRIPT_DESCRIPTION': ('%s scripting API test' %
+ self.language.capitalize()),
+ 'SCRIPT_LANGUAGE': self.language,
+ }
+ # count the total number of tests
+ tests_count = 0
+ for node in ast.walk(self.tree):
+ if isinstance(node, ast.Call) and \
+ isinstance(node.func, ast.Name) and \
+ node.func.id == 'check':
+ tests_count += 1
+ variables['SCRIPT_TESTS'] = str(tests_count)
+ # replace variables
+ for node in ast.walk(self.tree):
+ if isinstance(node, ast.Str) and \
+ node.s in variables:
+ node.s = variables[node.s]
+
+ def write_header(self, output):
+ """Generate script header (just comments by default)."""
+ comments = (
+ '',
+ '%s -- WeeChat %s scripting API testing' % (
+ self.script_name, self.language.capitalize()),
+ '',
+ 'WeeChat script automatically generated by testapigen.py.',
+ 'DO NOT EDIT BY HAND!',
+ '',
+ 'Date: %s' % datetime.now(),
+ '',
+ )
+ for line in comments:
+ output.write(self.comment(line).rstrip() + '\n')
+
+ def write(self):
+ """Write script on disk."""
+ print('Writing script %s... ' % self.script_path, end='')
+ with open(self.script_path, 'w') as output:
+ self.write_header(output)
+ self.unparse_class(output).add(self.tree)
+ output.write('\n')
+ self.write_footer(output)
+ print('OK')
+
+ def write_footer(self, output):
+ """Write footer (nothing by default)."""
+ pass
+
+
+class WeechatPythonScript(WeechatScript):
+ """A WeeChat script written in Python."""
+
+ def __init__(self, tree, source_script, output_dir):
+ super(WeechatPythonScript, self).__init__(
+ UnparsePython, tree, source_script, output_dir, 'python', 'py')
+
+ def rename_functions(self):
+ # nothing to rename in Python
+ pass
+
+ def write_header(self, output):
+ output.write('# -*- coding: utf-8 -*-\n')
+ super(WeechatPythonScript, self).write_header(output)
+ output.write('\n'
+ 'import weechat')
+
+ def write_footer(self, output):
+ output.write('\n'
+ '\n'
+ 'if __name__ == "__main__":\n'
+ ' weechat_init()\n')
+
+
+class WeechatPerlScript(WeechatScript):
+ """A WeeChat script written in Perl."""
+
+ def __init__(self, tree, source_script, output_dir):
+ super(WeechatPerlScript, self).__init__(
+ UnparsePerl, tree, source_script, output_dir, 'perl', 'pl')
+
+ def write_footer(self, output):
+ output.write('\n'
+ 'weechat_init();\n')
+
+
+class WeechatRubyScript(WeechatScript):
+ """A WeeChat script written in Ruby."""
+
+ def __init__(self, tree, source_script, output_dir):
+ super(WeechatRubyScript, self).__init__(
+ UnparseRuby, tree, source_script, output_dir, 'ruby', 'rb')
+
+ def rename_functions(self):
+ super(WeechatRubyScript, self).rename_functions()
+ for node in ast.walk(self.tree):
+ if isinstance(node, ast.Attribute) and \
+ node.value.id == 'weechat':
+ node.value.id = 'Weechat'
+
+
+class WeechatLuaScript(WeechatScript):
+ """A WeeChat script written in Lua."""
+
+ def __init__(self, tree, source_script, output_dir):
+ super(WeechatLuaScript, self).__init__(
+ UnparseLua, tree, source_script, output_dir, 'lua', 'lua',
+ comment_char='--')
+
+ def write_footer(self, output):
+ output.write('\n'
+ 'weechat_init()\n')
+
+
+class WeechatTclScript(WeechatScript):
+ """A WeeChat script written in Tcl."""
+
+ def __init__(self, tree, source_script, output_dir):
+ super(WeechatTclScript, self).__init__(
+ UnparseTcl, tree, source_script, output_dir, 'tcl', 'tcl')
+
+ def write_footer(self, output):
+ output.write('\n'
+ 'weechat_init\n')
+
+
+class WeechatGuileScript(WeechatScript):
+ """A WeeChat script written in Guile (Scheme)."""
+
+ def __init__(self, tree, source_script, output_dir):
+ super(WeechatGuileScript, self).__init__(
+ UnparseGuile, tree, source_script, output_dir, 'guile', 'scm',
+ comment_char=';')
+
+ def write_footer(self, output):
+ output.write('\n'
+ '(weechat_init)\n')
+
+
+class WeechatJavascriptScript(WeechatScript):
+ """A WeeChat script written in Javascript."""
+
+ def __init__(self, tree, source_script, output_dir):
+ super(WeechatJavascriptScript, self).__init__(
+ UnparseJavascript, tree, source_script, output_dir,
+ 'javascript', 'js', comment_char='//')
+
+ def write_footer(self, output):
+ output.write('\n'
+ 'weechat_init()\n')
+
+
+class WeechatPhpScript(WeechatScript):
+ """A WeeChat script written in PHP."""
+
+ def __init__(self, tree, source_script, output_dir):
+ super(WeechatPhpScript, self).__init__(
+ UnparsePhp, tree, source_script, output_dir, 'php', 'php',
+ comment_char='//')
+
+ def write_header(self, output):
+ output.write('<?php\n')
+ super(WeechatPhpScript, self).write_header(output)
+
+ def write_footer(self, output):
+ output.write('\n'
+ 'weechat_init();\n')
+
+# ============================================================================
+
+
+def update_nodes(tree):
+ """
+ Update the tests AST tree (in-place):
+ 1. add a print message in each test_* function
+ 2. add arguments in calls to check() function
+ """
+ for node in ast.walk(tree):
+ if isinstance(node, ast.FunctionDef) and \
+ node.name.startswith('test_'):
+ # add a print at the beginning of each test function
+ node.body.insert(
+ 0, ast.parse('weechat.prnt("", " > %s");' % node.name))
+ elif isinstance(node, ast.Call) and \
+ isinstance(node.func, ast.Name) and \
+ node.func.id == 'check':
+ # add two arguments in the call to "check" function:
+ # 1. the string representation of the test
+ # 2. the line number in source (as string)
+ # for example if this test is on line 50:
+ # check(weechat.test() == 123)
+ # it becomes:
+ # check(weechat.test() == 123, 'weechat.test() == 123', '50')
+ output = StringIO()
+ unparsed = UnparsePython(output=output)
+ unparsed.add(node.args[0])
+ node.args.append(ast.Str(output.getvalue()))
+ node.args.append(ast.Str(str(node.func.lineno)))
+
+
+def get_tests(path):
+ """Parse the source with tests and return the AST node."""
+ test_script = open(path).read()
+ tests = ast.parse(test_script)
+ update_nodes(tests)
+ return tests
+
+
+def generate_scripts(source_script, output_dir):
+ """Generate scripts in all languages to test the API."""
+ ret_code = 0
+ error = None
+ try:
+ for name, obj in inspect.getmembers(sys.modules[__name__]):
+ if inspect.isclass(obj) and name != 'WeechatScript' and \
+ name.startswith('Weechat') and name.endswith('Script'):
+ tests = get_tests(source_script)
+ obj(tests, source_script, output_dir).write()
+ except Exception as exc: # pylint: disable=broad-except
+ ret_code = 1
+ error = 'ERROR: %s\n\n%s' % (str(exc), traceback.format_exc())
+ return ret_code, error
+
+
+def testapigen_cmd_cb(data, buf, args):
+ """Callback for WeeChat command /testapigen."""
+
+ def print_error(msg):
+ """Print an error message on core buffer."""
+ weechat.prnt('', '%s%s' % (weechat.prefix('error'), msg))
+
+ try:
+ source_script, output_dir = args.split()
+ except ValueError:
+ print_error('ERROR: invalid arguments for /testapigen')
+ return weechat.WEECHAT_RC_OK
+ if not weechat.mkdir_parents(output_dir, 0o755):
+ print_error('ERROR: invalid directory: %s' % output_dir)
+ return weechat.WEECHAT_RC_OK
+ ret_code, error = generate_scripts(source_script, output_dir)
+ if error:
+ print_error(error)
+ return weechat.WEECHAT_RC_OK if ret_code == 0 else weechat.WEECHAT_RC_ERROR
+
+
+def get_parser_args():
+ """Get parser arguments."""
+ parser = argparse.ArgumentParser(
+ description=('Generate WeeChat scripts in all languages '
+ 'to test the API.'))
+ parser.add_argument(
+ 'script',
+ help='the path to Python script with tests')
+ parser.add_argument(
+ '-o', '--output-dir',
+ default='.',
+ help='output directory (defaults to current directory)')
+ return parser.parse_args()
+
+
+def main():
+ """Main function (when script is not loaded in WeeChat)."""
+ args = get_parser_args()
+ ret_code, error = generate_scripts(args.script, args.output_dir)
+ if error:
+ print(error)
+ sys.exit(ret_code)
+
+
+if __name__ == '__main__':
+ if RUNNING_IN_WEECHAT:
+ weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION,
+ SCRIPT_LICENSE, SCRIPT_DESC, '', '')
+ weechat.hook_command(
+ SCRIPT_COMMAND,
+ 'Generate scripting API test scripts',
+ 'source_script output_dir',
+ 'source_script: path to source script (testapi.py)\n'
+ ' output_dir: output directory for scripts',
+ '',
+ 'testapigen_cmd_cb', '')
+ else:
+ main()
diff --git a/tests/scripts/python/unparse.py b/tests/scripts/python/unparse.py
new file mode 100755
index 000000000..4176bd340
--- /dev/null
+++ b/tests/scripts/python/unparse.py
@@ -0,0 +1,1259 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2017 Sébastien Helleu <flashcode@flashtux.org>
+#
+# This program 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 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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, see <http://www.gnu.org/licenses/>.
+#
+
+"""
+Unparse AST tree to generate scripts in all supported languages
+(Python, Perl, Ruby, ...).
+"""
+
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import argparse
+import ast
+import inspect
+import os
+import select
+try:
+ from StringIO import StringIO # python 2
+except ImportError:
+ from io import StringIO # python 3
+import sys
+
+sys.dont_write_bytecode = True
+
+
+class UnparsePython(object):
+ """
+ Unparse AST to generate Python script code.
+
+ This class is inspired from unparse.py in cpython:
+ https://github.com/python/cpython/blob/master/Tools/parser/unparse.py
+
+ Note: only part of AST types are supported (just the types used by
+ the script to test WeeChat scripting API).
+ """
+
+ def __init__(self, output=sys.stdout):
+ self.output = output
+ self.indent_string = ' ' * 4
+ self._indent_level = 0
+ self._prefix = [] # not used in Python, only in other languages
+ self.binop = {
+ 'Add': '+',
+ 'Sub': '-',
+ 'Mult': '*',
+ 'MatMult': '@',
+ 'Div': '/',
+ 'Mod': '%',
+ 'LShift': '<<',
+ 'RShift': '>>',
+ 'BitOr': '|',
+ 'BitXor': '^',
+ 'BitAnd': '&',
+ 'FloorDiv': '//',
+ 'Pow': '**',
+ }
+ self.unaryop = {
+ 'Invert': '~',
+ 'Not': 'not',
+ 'UAdd': '+',
+ 'USub': '-',
+ }
+ self.cmpop = {
+ 'Eq': '==',
+ 'NotEq': '!=',
+ 'Lt': '<',
+ 'LtE': '<=',
+ 'Gt': '>',
+ 'GtE': '>=',
+ 'Is': 'is',
+ 'IsNot': 'is not',
+ 'In': 'in',
+ 'NotIn': 'not in',
+ }
+
+ def fill(self, string=''):
+ """Add a new line and an indented string."""
+ self.add('\n%s%s' % (self.indent_string * self._indent_level, string))
+
+ def indent(self):
+ """Indent code."""
+ self._indent_level += 1
+
+ def unindent(self):
+ """Unindent code."""
+ self._indent_level -= 1
+
+ def prefix(self, prefix):
+ """Add or remove a prefix from list."""
+ if prefix:
+ self._prefix.append(prefix)
+ else:
+ self._prefix.pop()
+
+ def add(self, *args):
+ """Add string/node(s) to the output file."""
+ for arg in args:
+ if callable(arg):
+ arg()
+ elif isinstance(arg, tuple):
+ arg[0](*arg[1:])
+ elif isinstance(arg, list):
+ for item in arg:
+ self.add(item)
+ elif isinstance(arg, ast.AST):
+ method = getattr(
+ self, '_ast_%s' % arg.__class__.__name__.lower(),
+ None)
+ if method is None:
+ raise NotImplementedError(arg)
+ method(arg)
+ elif isinstance(arg, str):
+ self.output.write(arg)
+
+ @staticmethod
+ def make_list(values, sep=', '):
+ """Add multiple values using a custom method and separator."""
+ result = []
+ for value in values:
+ if result:
+ result.append(sep)
+ result.append(value)
+ return result
+
+ def is_bool(self, node): # pylint: disable=no-self-use
+ """Check if the node is a boolean."""
+ return isinstance(node, ast.Name) and node.id in ('False', 'True')
+
+ def is_number(self, node): # pylint: disable=no-self-use
+ """Check if the node is a number."""
+ # in python 2, number -1 is Num(n=-1)
+ # in Python 3, number -1 is UnaryOp(op=USub(), operand=Num(n=1))
+ return (isinstance(node, ast.Num) or
+ (isinstance(node, ast.UnaryOp) and
+ isinstance(node.op, (ast.UAdd, ast.USub))))
+
+ def _ast_alias(self, node):
+ """Add an AST alias in output."""
+ # ignore alias
+ pass
+
+ def _ast_arg(self, node):
+ """Add an AST arg in output."""
+ self.add('%s%s' % (self._prefix[-1] if self._prefix else '',
+ node.arg))
+
+ def _ast_assign(self, node):
+ """Add an AST Assign in output."""
+ self.add(
+ self.fill,
+ [[target, ' = '] for target in node.targets],
+ node.value,
+ )
+
+ def _ast_attribute(self, node):
+ """Add an AST Attribute in output."""
+ self.add(node.value, '.', node.attr)
+
+ def _ast_binop(self, node):
+ """Add an AST BinOp in output."""
+ self.add(
+ node.left,
+ ' %s ' % self.binop[node.op.__class__.__name__],
+ node.right,
+ )
+
+ def _ast_call(self, node):
+ """Add an AST Call in output."""
+ self.add(
+ node.func,
+ '(',
+ self.make_list(node.args),
+ ')',
+ )
+
+ def _ast_compare(self, node):
+ """Add an AST Compare in output."""
+ self.add(node.left)
+ for operator, comparator in zip(node.ops, node.comparators):
+ self.add(
+ ' %s ' % self.cmpop[operator.__class__.__name__],
+ comparator,
+ )
+
+ def _ast_dict(self, node):
+ """Add an AST Dict in output."""
+ self.add(
+ '{',
+ self.make_list([[key, ': ', value]
+ for key, value in zip(node.keys, node.values)]),
+ '}',
+ )
+
+ def _ast_expr(self, node):
+ """Add an AST Expr in output."""
+ if not isinstance(node.value, ast.Str): # ignore docstrings
+ self.add(
+ self.fill,
+ node.value,
+ )
+
+ def _ast_functiondef(self, node):
+ """Add an AST FunctionDef in output."""
+ self.add(
+ self.fill,
+ self.fill,
+ self.fill if self._indent_level == 0 else None,
+ 'def %s(' % node.name,
+ self.make_list([arg for arg in node.args.args]),
+ '):',
+ self.indent,
+ node.body,
+ self.unindent,
+ )
+
+ def _ast_if(self, node):
+ """Add an AST If in output."""
+ self.add(
+ self.fill,
+ 'if ',
+ node.test,
+ ':',
+ self.indent,
+ node.body,
+ self.unindent,
+ )
+ if node.orelse:
+ self.add(
+ self.fill,
+ 'else:',
+ self.indent,
+ node.orelse,
+ self.unindent,
+ )
+
+ def _ast_import(self, node):
+ """Add an AST Import in output."""
+ # ignore import
+ pass
+
+ def _ast_module(self, node):
+ """Add an AST Module in output."""
+ self.add(node.body)
+
+ def _ast_name(self, node):
+ """Add an AST Name in output."""
+ self.add('%s%s' % (self._prefix[-1] if self._prefix else '',
+ node.id))
+
+ def _ast_num(self, node):
+ """Add an AST Num in output."""
+ self.add(repr(node.n))
+
+ def _ast_pass(self, node): # pylint: disable=unused-argument
+ """Add an AST Pass in output."""
+ self.fill('pass')
+
+ def _ast_return(self, node):
+ """Add an AST Return in output."""
+ self.fill('return')
+ if node.value:
+ self.add(' ', node.value)
+
+ def _ast_str(self, node):
+ """Add an AST Str in output."""
+ self.add(repr(node.s))
+
+ def _ast_tuple(self, node):
+ """Add an AST Tuple in output."""
+ self.add(
+ '(',
+ self.make_list(node.elts),
+ ',' if len(node.elts) == 1 else None,
+ ')',
+ )
+
+ def _ast_unaryop(self, node):
+ """Add an AST UnaryOp in output."""
+ self.add(
+ '(',
+ self.unaryop[node.op.__class__.__name__],
+ ' ',
+ node.operand,
+ ')',
+ )
+
+
+class UnparsePerl(UnparsePython):
+ """
+ Unparse AST to generate Perl script code.
+
+ Note: only part of AST types are supported (just the types used by
+ the script to test WeeChat scripting API).
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(UnparsePerl, self).__init__(*args, **kwargs)
+
+ def _ast_assign(self, node):
+ """Add an AST Assign in output."""
+ self.add(
+ self.fill,
+ (self.prefix, '%' if isinstance(node.value, ast.Dict) else '$'),
+ [[target, ' = '] for target in node.targets],
+ (self.prefix, None),
+ node.value,
+ ';',
+ )
+
+ def _ast_attribute(self, node):
+ """Add an AST Attribute in output."""
+ saved_prefix = self._prefix
+ self._prefix = []
+ self.add(node.value, '::', node.attr)
+ self._prefix = saved_prefix
+
+ def _ast_binop(self, node):
+ """Add an AST BinOp in output."""
+ if isinstance(node.op, ast.Add) and \
+ (isinstance(node.left, (ast.Name, ast.Str)) or
+ isinstance(node.right, (ast.Name, ast.Str))):
+ str_op = '.'
+ else:
+ str_op = self.binop[node.op.__class__.__name__]
+ self.add(
+ (self.prefix, '$'),
+ node.left,
+ ' %s ' % str_op,
+ node.right,
+ (self.prefix, None),
+ )
+
+ def _ast_call(self, node):
+ """Add an AST Call in output."""
+ self.add(
+ node.func,
+ '(',
+ (self.prefix, '$'),
+ self.make_list(node.args),
+ (self.prefix, None),
+ ')',
+ )
+
+ def _ast_compare(self, node):
+ """Add an AST Compare in output."""
+ self.add(node.left)
+ for operator, comparator in zip(node.ops, node.comparators):
+ if isinstance(operator, (ast.Eq, ast.NotEq)) and \
+ not self.is_number(node.left) and \
+ not self.is_bool(node.left) and \
+ not self.is_number(comparator) and \
+ not self.is_bool(comparator):
+ custom_cmpop = {
+ 'Eq': 'eq',
+ 'NotEq': 'ne',
+ }
+ else:
+ custom_cmpop = self.cmpop
+ self.add(
+ ' %s ' % custom_cmpop[operator.__class__.__name__],
+ comparator,
+ )
+
+ def _ast_dict(self, node):
+ """Add an AST Dict in output."""
+ self.add(
+ '{',
+ self.make_list([[key, ' => ', value]
+ for key, value in zip(node.keys, node.values)]),
+ '}',
+ )
+
+ def _ast_expr(self, node):
+ """Add an AST Expr in output."""
+ if not isinstance(node.value, ast.Str): # ignore docstrings
+ self.add(
+ self.fill,
+ node.value,
+ ';',
+ )
+
+ def _ast_functiondef(self, node):
+ """Add an AST FunctionDef in output."""
+ self.add(
+ self.fill,
+ self.fill,
+ 'sub %s' % node.name,
+ self.fill,
+ '{',
+ self.indent,
+ )
+ if node.args.args:
+ self.add(
+ self.fill,
+ 'my (',
+ (self.prefix, '$'),
+ self.make_list([arg for arg in node.args.args]),
+ (self.prefix, None),
+ ') = @_;',
+ )
+ self.add(
+ node.body,
+ self.unindent,
+ self.fill,
+ '}',
+ )
+
+ def _ast_if(self, node):
+ """Add an AST If in output."""
+ self.add(
+ self.fill,
+ 'if (',
+ (self.prefix, '$'),
+ node.test,
+ (self.prefix, None),
+ ')',
+ self.fill,
+ '{',
+ self.indent,
+ node.body,
+ self.unindent,
+ self.fill,
+ '}',
+ )
+ if node.orelse:
+ self.add(
+ self.fill,
+ 'else',
+ self.fill,
+ '{',
+ self.indent,
+ node.orelse,
+ self.unindent,
+ self.fill,
+ '}',
+ )
+
+ def _ast_pass(self, node):
+ """Add an AST Pass in output."""
+ pass
+
+ def _ast_str(self, node):
+ """Add an AST Str in output."""
+ self.add('"%s"' % node.s.replace('$', '\\$'))
+
+
+class UnparseRuby(UnparsePython):
+ """
+ Unparse AST to generate Ruby script code.
+
+ Note: only part of AST types are supported (just the types used by
+ the script to test WeeChat scripting API).
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(UnparseRuby, self).__init__(*args, **kwargs)
+
+ def _ast_attribute(self, node):
+ """Add an AST Attribute in output."""
+ self.add(
+ node.value,
+ '::' if node.attr.startswith('WEECHAT_') else '.',
+ node.attr,
+ )
+
+ def _ast_dict(self, node):
+ """Add an AST Dict in output."""
+ self.add(
+ 'Hash[',
+ self.make_list([[key, ' => ', value]
+ for key, value in zip(node.keys, node.values)]),
+ ']',
+ )
+
+ def _ast_functiondef(self, node):
+ """Add an AST FunctionDef in output."""
+ self.add(
+ self.fill,
+ self.fill,
+ 'def %s' % node.name,
+ )
+ if node.args.args:
+ self.add(
+ '(',
+ self.make_list([arg for arg in node.args.args]),
+ ')',
+ )
+ self.add(
+ self.indent,
+ node.body,
+ self.unindent,
+ self.fill,
+ 'end',
+ )
+
+ def _ast_if(self, node):
+ """Add an AST If in output."""
+ self.add(
+ self.fill,
+ 'if ',
+ node.test,
+ self.indent,
+ node.body,
+ self.unindent,
+ )
+ if node.orelse:
+ self.add(
+ self.fill,
+ 'else',
+ self.indent,
+ node.orelse,
+ self.unindent,
+ self.fill,
+ 'end',
+ )
+
+ def _ast_pass(self, node):
+ """Add an AST Pass in output."""
+ pass
+
+
+class UnparseLua(UnparsePython):
+ """
+ Unparse AST to generate Lua script code.
+
+ Note: only part of AST types are supported (just the types used by
+ the script to test WeeChat scripting API).
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(UnparseLua, self).__init__(*args, **kwargs)
+ self.cmpop = {
+ 'Eq': '==',
+ 'NotEq': '~=',
+ 'Lt': '<',
+ 'LtE': '<=',
+ 'Gt': '>',
+ 'GtE': '>=',
+ }
+ self._var_quotes = True
+
+ def _set_var_quotes(self, value):
+ """Set boolean to quote variables."""
+ self._var_quotes = value
+
+ def _ast_binop(self, node):
+ """Add an AST BinOp in output."""
+ if isinstance(node.op, ast.Add) and \
+ (isinstance(node.left, (ast.Name, ast.Str)) or
+ isinstance(node.right, (ast.Name, ast.Str))):
+ str_op = '..'
+ else:
+ str_op = self.binop[node.op.__class__.__name__]
+ self.add(
+ node.left,
+ ' %s ' % str_op,
+ node.right,
+ )
+
+ def _ast_dict(self, node):
+ """Add an AST Dict in output."""
+ self.add(
+ '{',
+ self.make_list([
+ [(self._set_var_quotes, False),
+ key,
+ (self._set_var_quotes, True),
+ '=',
+ value]
+ for key, value in zip(node.keys, node.values)]),
+ '}',
+ )
+
+ def _ast_functiondef(self, node):
+ """Add an AST FunctionDef in output."""
+ self.add(
+ self.fill,
+ self.fill,
+ 'function %s' % node.name,
+ )
+ self.add(
+ '(',
+ self.make_list([arg for arg in node.args.args]),
+ ')',
+ self.indent,
+ node.body,
+ self.unindent,
+ self.fill,
+ 'end',
+ )
+
+ def _ast_if(self, node):
+ """Add an AST If in output."""
+ self.add(
+ self.fill,
+ 'if ',
+ node.test,
+ ' then',
+ self.indent,
+ node.body,
+ self.unindent,
+ )
+ if node.orelse:
+ self.add(
+ self.fill,
+ 'else',
+ self.indent,
+ node.orelse,
+ self.unindent,
+ self.fill,
+ 'end',
+ )
+
+ def _ast_pass(self, node):
+ """Add an AST Pass in output."""
+ pass
+
+ def _ast_str(self, node):
+ """Add an AST Str in output."""
+ self.add(repr(node.s) if self._var_quotes else node.s)
+
+
+class UnparseTcl(UnparsePython):
+ """
+ Unparse AST to generate Tcl script code.
+
+ Note: only part of AST types are supported (just the types used by
+ the script to test WeeChat scripting API).
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(UnparseTcl, self).__init__(*args, **kwargs)
+ self._call = 0
+
+ def _ast_assign(self, node):
+ """Add an AST Assign in output."""
+ self.add(
+ self.fill,
+ 'set ',
+ node.targets[0],
+ ' [',
+ node.value,
+ ']',
+ )
+
+ def _ast_attribute(self, node):
+ """Add an AST Attribute in output."""
+ saved_prefix = self._prefix
+ self._prefix = []
+ if node.attr.startswith('WEECHAT_'):
+ self.add('$::')
+ self.add(node.value, '::', node.attr)
+ self._prefix = saved_prefix
+
+ def _ast_binop(self, node):
+ """Add an AST BinOp in output."""
+ self.add(
+ '[join [list ',
+ (self.prefix, '$'),
+ node.left,
+ ' ',
+ node.right,
+ (self.prefix, None),
+ '] ""]',
+ )
+
+ def _ast_call(self, node):
+ """Add an AST Call in output."""
+ if self._call:
+ self.add('[')
+ self._call += 1
+ self.add(
+ node.func,
+ ' ' if node.args else None,
+ (self.prefix, '$'),
+ self.make_list([arg for arg in node.args], sep=' '),
+ (self.prefix, None),
+ )
+ self._call -= 1
+ if self._call:
+ self.add(']')
+
+ def _ast_compare(self, node):
+ """Add an AST Compare in output."""
+ self.prefix('$')
+ if self._call:
+ self.add('[expr {')
+ self.add(node.left)
+ for operator, comparator in zip(node.ops, node.comparators):
+ if isinstance(operator, (ast.Eq, ast.NotEq)) and \
+ not self.is_number(node.left) and \
+ not self.is_bool(node.left) and \
+ not self.is_number(comparator) and \
+ not self.is_bool(comparator):
+ custom_cmpop = {
+ 'Eq': 'eq',
+ 'NotEq': 'ne',
+ }
+ else:
+ custom_cmpop = self.cmpop
+ self.add(
+ ' %s ' % custom_cmpop[operator.__class__.__name__],
+ comparator,
+ )
+ if self._call:
+ self.add('}]')
+ self.prefix(None)
+
+ def _ast_dict(self, node):
+ """Add an AST Dict in output."""
+ self.add(
+ '[dict create ',
+ self.make_list([[key, ' ', value]
+ for key, value in zip(node.keys, node.values)],
+ sep=' '),
+ ']',
+ )
+
+ def _ast_functiondef(self, node):
+ """Add an AST FunctionDef in output."""
+ self.add(
+ self.fill,
+ self.fill,
+ 'proc %s {' % node.name,
+ (self.make_list([arg for arg in node.args.args], sep=' ')
+ if node.args.args else None),
+ '} {',
+ self.indent,
+ node.body,
+ self.unindent,
+ self.fill,
+ '}',
+ )
+
+ def _ast_if(self, node):
+ """Add an AST If in output."""
+ self.add(
+ self.fill,
+ 'if {',
+ (self.prefix, '$'),
+ node.test,
+ (self.prefix, None),
+ '} {',
+ self.indent,
+ node.body,
+ self.unindent,
+ self.fill,
+ '}',
+ )
+ if node.orelse:
+ self.add(
+ ' else {',
+ self.indent,
+ node.orelse,
+ self.unindent,
+ self.fill,
+ '}',
+ )
+
+ def _ast_pass(self, node):
+ """Add an AST Pass in output."""
+ pass
+
+ def _ast_str(self, node):
+ """Add an AST Str in output."""
+ self.add('"%s"' % node.s.replace('$', '\\$'))
+
+
+class UnparseGuile(UnparsePython):
+ """
+ Unparse AST to generate Guile script code.
+
+ Note: only part of AST types are supported (just the types used by
+ the script to test WeeChat scripting API).
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(UnparseGuile, self).__init__(*args, **kwargs)
+ self.cmpop = {
+ 'Eq': '=',
+ 'NotEq': '<>',
+ 'Lt': '<',
+ 'LtE': '<=',
+ 'Gt': '>',
+ 'GtE': '>=',
+ }
+ self._call = 0
+ self._let = 0
+
+ def _ast_assign(self, node):
+ """Add an AST Assign in output."""
+ self.add(
+ self.fill,
+ '(let ((',
+ node.targets[0],
+ ' ',
+ node.value,
+ '))',
+ self.indent,
+ self.fill,
+ '(begin',
+ self.indent,
+ )
+ self._let += 1
+
+ def _ast_attribute(self, node):
+ """Add an AST Attribute in output."""
+ self.add(node.value, ':', node.attr)
+
+ def _ast_binop(self, node):
+ """Add an AST BinOp in output."""
+ if isinstance(node.op, ast.Add) and \
+ (isinstance(node.left, (ast.Name, ast.Str)) or
+ isinstance(node.right, (ast.Name, ast.Str))):
+ self.add(
+ '(string-append ',
+ node.left,
+ ' ',
+ node.right,
+ ')',
+ )
+ else:
+ self.add(
+ node.left,
+ ' %s ' % self.binop[node.op.__class__.__name__],
+ node.right,
+ )
+
+ def _ast_call(self, node):
+ """Add an AST Call in output."""
+ self._call += 1
+ self.add(
+ '(',
+ node.func,
+ ' ' if node.args else None,
+ self.make_list([arg for arg in node.args], sep=' '),
+ ')',
+ )
+ self._call -= 1
+
+ def _ast_compare(self, node):
+ """Add an AST Compare in output."""
+ for operator, comparator in zip(node.ops, node.comparators):
+ if isinstance(operator, (ast.Eq, ast.NotEq)) and \
+ not self.is_number(node.left) and \
+ not self.is_bool(node.left) and \
+ not self.is_number(comparator) and \
+ not self.is_bool(comparator):
+ prefix = 'string'
+ else:
+ prefix = ''
+ self.add(
+ '(%s%s ' % (prefix, self.cmpop[operator.__class__.__name__]),
+ node.left,
+ ' ',
+ comparator,
+ ')',
+ )
+
+ def _ast_dict(self, node):
+ """Add an AST Dict in output."""
+ self.add(
+ '\'(',
+ self.make_list([['(', key, ' ', value, ')']
+ for key, value in zip(node.keys, node.values)],
+ sep=' '),
+ ')',
+ )
+
+ def _ast_functiondef(self, node):
+ """Add an AST FunctionDef in output."""
+ self.add(
+ self.fill,
+ self.fill,
+ '(define (%s' % node.name,
+ ' ' if node.args.args else None,
+ (self.make_list([arg for arg in node.args.args], sep=' ')
+ if node.args.args else None),
+ ')',
+ self.indent,
+ node.body,
+ )
+ while self._let > 0:
+ self.add(
+ self.unindent,
+ self.fill,
+ ')',
+ self.unindent,
+ self.fill,
+ ')',
+ )
+ self._let -= 1
+ self.add(
+ self.unindent,
+ self.fill,
+ ')',
+ )
+
+ def _ast_if(self, node):
+ """Add an AST If in output."""
+ self.add(
+ self.fill,
+ '(if '
+ '' if isinstance(node.test, ast.Name) else '(',
+ node.test,
+ '' if isinstance(node.test, ast.Name) else ')',
+ self.indent,
+ self.fill,
+ '(begin',
+ self.indent,
+ node.body,
+ self.unindent,
+ self.fill,
+ ')',
+ self.unindent,
+ )
+ if node.orelse:
+ self.add(
+ self.indent,
+ self.fill,
+ '(begin',
+ self.indent,
+ node.orelse,
+ self.unindent,
+ self.fill,
+ ')',
+ self.unindent,
+ )
+ self.add(self.fill, ')')
+
+ def _ast_pass(self, node):
+ """Add an AST Pass in output."""
+ pass
+
+ def _ast_str(self, node):
+ """Add an AST Str in output."""
+ self.add('"%s"' % node.s)
+
+
+class UnparseJavascript(UnparsePython):
+ """
+ Unparse AST to generate Javascript script code.
+
+ Note: only part of AST types are supported (just the types used by
+ the script to test WeeChat scripting API).
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(UnparseJavascript, self).__init__(*args, **kwargs)
+
+ def _ast_dict(self, node):
+ """Add an AST Dict in output."""
+ self.add(
+ '{',
+ self.make_list([[key, ': ', value]
+ for key, value in zip(node.keys, node.values)]),
+ '}',
+ )
+
+ def _ast_functiondef(self, node):
+ """Add an AST FunctionDef in output."""
+ self.add(
+ self.fill,
+ self.fill,
+ 'function %s(' % node.name,
+ self.make_list([arg for arg in node.args.args]),
+ ') {',
+ self.indent,
+ node.body,
+ self.unindent,
+ self.fill,
+ '}',
+ )
+
+ def _ast_if(self, node):
+ """Add an AST If in output."""
+ self.add(
+ self.fill,
+ 'if (',
+ node.test,
+ ') {',
+ self.indent,
+ node.body,
+ self.unindent,
+ self.fill,
+ '}',
+ )
+ if node.orelse:
+ self.add(
+ ' else {',
+ self.indent,
+ node.orelse,
+ self.unindent,
+ self.fill,
+ '}',
+ )
+
+ def _ast_pass(self, node):
+ """Add an AST Pass in output."""
+ pass
+
+
+class UnparsePhp(UnparsePython):
+ """
+ Unparse AST to generate PHP script code.
+
+ Note: only part of AST types are supported (just the types used by
+ the script to test WeeChat scripting API).
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(UnparsePhp, self).__init__(*args, **kwargs)
+
+ def _ast_assign(self, node):
+ """Add an AST Assign in output."""
+ self.add(
+ self.fill,
+ (self.prefix, '$'),
+ [[target, ' = '] for target in node.targets],
+ (self.prefix, None),
+ node.value,
+ ';',
+ )
+
+ def _ast_attribute(self, node):
+ """Add an AST Attribute in output."""
+ saved_prefix = self._prefix
+ self._prefix = []
+ if not node.attr.startswith('WEECHAT_'):
+ self.add(node.value, '_')
+ self.add(node.attr)
+ self._prefix = saved_prefix
+
+ def _ast_binop(self, node):
+ """Add an AST BinOp in output."""
+ if isinstance(node.op, ast.Add) and \
+ (isinstance(node.left, (ast.Name, ast.Str)) or
+ isinstance(node.right, (ast.Name, ast.Str))):
+ str_op = '.'
+ else:
+ str_op = self.binop[node.op.__class__.__name__]
+ self.add(
+ (self.prefix, '$'),
+ node.left,
+ ' %s ' % str_op,
+ node.right,
+ (self.prefix, None),
+ )
+
+ def _ast_call(self, node):
+ """Add an AST Call in output."""
+ self.add(
+ node.func,
+ '(',
+ (self.prefix, '$'),
+ self.make_list(node.args),
+ (self.prefix, None),
+ ')',
+ )
+
+ def _ast_dict(self, node):
+ """Add an AST Dict in output."""
+ self.add(
+ 'array(',
+ self.make_list([[key, ' => ', value]
+ for key, value in zip(node.keys, node.values)]),
+ ')',
+ )
+
+ def _ast_expr(self, node):
+ """Add an AST Expr in output."""
+ if not isinstance(node.value, ast.Str): # ignore docstrings
+ self.add(
+ self.fill,
+ node.value,
+ ';',
+ )
+
+ def _ast_functiondef(self, node):
+ """Add an AST FunctionDef in output."""
+ self.add(
+ self.fill,
+ self.fill,
+ 'function %s(' % node.name,
+ (self.prefix, '$'),
+ self.make_list([arg for arg in node.args.args]),
+ (self.prefix, None),
+ ')',
+ self.fill,
+ '{',
+ self.indent,
+ node.body,
+ self.unindent,
+ self.fill,
+ '}',
+ )
+
+ def _ast_if(self, node):
+ """Add an AST If in output."""
+ self.add(
+ self.fill,
+ 'if (',
+ (self.prefix, '$'),
+ node.test,
+ (self.prefix, None),
+ ')',
+ self.fill,
+ '{',
+ self.indent,
+ node.body,
+ self.unindent,
+ self.fill,
+ '}',
+ )
+ if node.orelse:
+ self.add(
+ self.fill,
+ 'else',
+ self.fill,
+ '{',
+ self.indent,
+ node.orelse,
+ self.unindent,
+ self.fill,
+ '}',
+ )
+
+ def _ast_pass(self, node):
+ """Add an AST Pass in output."""
+ pass
+
+ def _ast_str(self, node):
+ """Add an AST Str in output."""
+ self.add('"%s"' % node.s.replace('$', '\\$'))
+
+
+def get_languages():
+ """Return a list of supported languages: ['python', 'perl', ...]."""
+
+ def linenumber_of_member(member):
+ """Return the line number of a member."""
+ try:
+ # python 2
+ return member[1].__init__.im_func.func_code.co_firstlineno
+ except AttributeError:
+ try:
+ # python 3
+ return member[1].__init__.__code__.co_firstlineno
+ except AttributeError:
+ return -1
+
+ languages = []
+ members = inspect.getmembers(sys.modules[__name__],
+ predicate=inspect.isclass)
+ members.sort(key=linenumber_of_member)
+ for name, obj in members:
+ if inspect.isclass(obj) and name.startswith('Unparse'):
+ languages.append(name[7:].lower())
+
+ return languages
+
+
+LANGUAGES = get_languages()
+
+
+def get_parser():
+ """Get parser arguments."""
+ all_languages = LANGUAGES + ['all']
+ default_language = LANGUAGES[0]
+ parser = argparse.ArgumentParser(
+ description=('Unparse Python code from stdin and generate code in '
+ 'another language (to stdout).\n\n'
+ 'The code is read from stdin and generated code is '
+ 'written on stdout.'))
+ parser.add_argument(
+ '-l', '--language',
+ default=default_language,
+ choices=all_languages,
+ help='output language (default: %s)' % default_language)
+ return parser
+
+
+def get_stdin():
+ """
+ Return data from standard input.
+
+ If there is nothing in stdin, wait for data until ctrl-D (EOF)
+ is received.
+ """
+ data = ''
+ inr = select.select([sys.stdin], [], [], 0)[0]
+ if not inr:
+ print('Enter the code to convert (Enter + ctrl+D to end)')
+ while True:
+ inr = select.select([sys.stdin], [], [], 0.1)[0]
+ if not inr:
+ continue
+ new_data = os.read(sys.stdin.fileno(), 4096)
+ if not new_data: # EOF?
+ break
+ data += new_data.decode('utf-8')
+ return data
+
+
+def convert_to_language(code, language, prefix=''):
+ """Convert Python code to a language."""
+ class_name = 'Unparse%s' % language.capitalize()
+ unparse_class = getattr(sys.modules[__name__], class_name)
+ if prefix:
+ print(prefix)
+ output = StringIO()
+ unparse_class(output=output).add(ast.parse(code))
+ print(output.getvalue().lstrip())
+
+
+def convert(code, language):
+ """Convert Python code to one or all languages."""
+ if language == 'all':
+ for lang in LANGUAGES:
+ convert_to_language(code, lang, '\n%s:' % lang)
+ else:
+ convert_to_language(code, language)
+
+
+def main():
+ """Main function."""
+ parser = get_parser()
+ args = parser.parse_args()
+
+ code = get_stdin()
+ if not code:
+ print('ERROR: missing input')
+ print()
+ parser.print_help()
+ sys.exit(1)
+
+ convert(code, args.language)
+
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ main()