diff options
author | Sébastien Helleu <flashcode@flashtux.org> | 2017-10-07 16:51:25 +0200 |
---|---|---|
committer | Sébastien Helleu <flashcode@flashtux.org> | 2017-10-07 16:51:25 +0200 |
commit | e8af8536246bbda3289e70637f40dcba3c87977f (patch) | |
tree | e111538e2b5d8effd0db0bc7ca618743a73bbe02 /tests/scripts/python | |
parent | f6fe6be7a4998ded6b6b4a7dbb9d1d12f3e8f65a (diff) | |
download | weechat-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.py | 131 | ||||
-rwxr-xr-x | tests/scripts/python/testapigen.py | 408 | ||||
-rwxr-xr-x | tests/scripts/python/unparse.py | 1259 |
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() |